/**
 * 
 * Copyright 2005 LogicBlaze, Inc. http://www.logicblaze.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/
package net.bingosoft.common.upload;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 实现一个超出指定时间后就会移除对象的简单的Map
 */
public class TimeoutMap implements Runnable {

    private static final Logger log = LoggerFactory.getLogger(TimeoutMap.class);

    private Map<Object, Object> map = new HashMap<Object, Object>();
    
    private SortedSet<Entry> index = new TreeSet<Entry>();
    
    private ScheduledExecutorService executor;
    
    private long purgePollTime;
    
    private TimeOutListener listener;
    
    private boolean scheduled = false;

    public TimeoutMap() {
        this(Executors.newScheduledThreadPool(1), 1000L);
    }

    public TimeoutMap(long requestMapPollTimeMillis) {
        this(Executors.newScheduledThreadPool(1), requestMapPollTimeMillis);
    }    
    
    public TimeoutMap(ScheduledExecutorService executor, long requestMapPollTimeMillis) {
        this.executor = executor;
        this.purgePollTime = requestMapPollTimeMillis;
    }

    public Object get(Object key) {
        Entry entry = null;
        synchronized (map) {
            entry = (Entry) map.get(key);
            if (entry == null) {
                return null;
            }
        }
        return entry.getValue();
    }

    public void put(Object key, Object value, long timeoutMillis) {
        Entry entry = new Entry(key, value, timeoutMillis);
        synchronized (map) {
            Object oldValue = map.put(key, entry);
            if (oldValue != null) {
                index.remove(oldValue);
            }
            updateExpireTime(entry);
            index.add(entry);
            
            if(log.isDebugEnabled()){
                log.debug("object {}:{} put to this map",key,null == value ? "null" : value.getClass().getSimpleName());
            }
            
            schedulePoll();
        }
    }

    public void remove(Object id) {
        synchronized (map) {
            Entry entry = (Entry) map.remove(id);
            if (entry != null) {
                index.remove(entry);
            }
        }
    }

    /**
     * Returns a copy of the keys in the map
     */
    public Object[] getKeys() {
        Object[] keys = null;
        synchronized (map) {
            Set<Object> keySet = map.keySet();
            keys = new String[keySet.size()];
            keySet.toArray(keys);
        }
        return keys;
    }

    /**
     * The timer task which purges old requests and schedules another poll
     */
    public void run() {
        purge();
        scheduled = false;
        schedulePoll();
    }

    /**
     * Purges any old entries from the map
     */
    public void purge() {
        long now = currentTime();
        
        log.debug("start purge cached objects,current time:{}",now);
        
        long expired = 0;
        synchronized (map) {
            for (Iterator<Entry> iter = index.iterator(); iter.hasNext();) {
                Entry entry = (Entry) iter.next();
                if (entry == null) {
                    log.debug("has no any entries in cached map,break");
                    break;
                }
                if(entry.getTimeout() < 0){
                    log.debug("entry cached for ever,break");
                    break;
                }
                if ( entry.getExpireTime() < now) {
                    log.debug("found a expired entry '{}',expire time {}",entry.getKey(),entry.getExpireTime());
                    if (isValidForEviction(entry)) {
                        expired++;
                        if (log.isDebugEnabled()) {
                            log.debug("Evicting inactive request for correlationID: {}",entry);
                        }
                        Object value = map.remove(entry.getKey());
                        iter.remove();
                        if(null != this.listener){
                            this.listener.purge(entry.getKey(), value);
                        }
                    }
                } else {
                    break;
                }
            }
        }
        
        log.debug("purge finished,current time:{},found {} expired objects",currentTime(),expired);
    }

    // Properties
    // -------------------------------------------------------------------------
    public long getPurgePollTime() {
        return purgePollTime;
    }

    /**
     * Sets the next purge poll time in milliseconds
     */
    public void setPurgePollTime(long purgePollTime) {
        this.purgePollTime = purgePollTime;
    }
    
    public void setListener(TimeOutListener listener) {
        this.listener = listener;
    }

    public ScheduledExecutorService getExecutor() {
        return executor;
    }

    /**
     * Sets the executor used to schedule purge events of inactive requests
     */
    public void setExecutor(ScheduledExecutorService executor) {
        this.executor = executor;
    }

    // Implementation methods
    // -------------------------------------------------------------------------

    /**
     * lets schedule each time to allow folks to change the time at runtime
     */
    protected synchronized void schedulePoll() {
        if (!scheduled && executor != null && !this.index.isEmpty()) {
            log.debug("schedule {} ms to do the executor",purgePollTime);
            executor.schedule(this, purgePollTime, TimeUnit.MILLISECONDS);
            
            scheduled = true;
        }else{
            scheduled = false;   
        }
    }

    /**
     * A hook to allow derivations to avoid evicting the current entry
     * 
     * @param entry
     * @return
     */
    protected boolean isValidForEviction(Entry entry) {
        return true;
    }

    protected void updateExpireTime(Entry entry) {
        long now = currentTime();
        entry.setExpireTime(entry.getTimeout() + now);
    }

    protected long currentTime() {
        return System.currentTimeMillis();
    }

    public static class Entry implements Comparable<Object>, Map.Entry<Object, Object> {
        private Object key;
        private Object value;
        private long timeout;
        private long expireTime;

        public Entry(Object id, Object handler, long timeout) {
            this.key = id;
            this.value = handler;
            this.timeout = timeout;
        }

        public Object getKey() {
            return key;
        }

        public long getExpireTime() {
            return expireTime;
        }

        public void setExpireTime(long expireTime) {
            this.expireTime = expireTime;
        }

        public Object getValue() {
            return value;
        }

        public Object setValue(Object value) {
            Object oldValue = value;
            this.value = value;
            return oldValue;
        }

        public long getTimeout() {
            return timeout;
        }

        public void setTimeout(long timeout) {
            this.timeout = timeout;
        }

        public int compareTo(Object that) {
            if (this == that) {
                return 0;
            }
            if (that instanceof Entry) {
                return compareTo((Entry) that);
            }
            return 1;
        }

        public int compareTo(Entry that) {
            long diff = this.expireTime - that.expireTime;
            if (diff > 0) {
                return 1;
            } else if (diff < 0) {
                return -1;
            }
            return this.key.hashCode() - that.key.hashCode();
        }

        public String toString() {
            return "Entry for key: " + key;
        }
    }
    
    public static interface TimeOutListener{
        void purge(Object Key,Object value);
    }
}
